package org.makumba.providers.datadefinition.mdd;

import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Vector;

import org.makumba.DataDefinitionNotFoundError;
import org.makumba.commons.ReservedKeywords;
import org.makumba.providers.query.mql.HqlParser;
import org.makumba.providers.query.mql.Node;

import antlr.RecognitionException;
import antlr.TokenStream;
import antlr.TokenStreamException;
import antlr.collections.AST;

/**
 * MDD Parser extending the parser generated by ANTLR for performing specific pre-processing operations
 * 
 * @author Manuel Bernhardt <manuel@makumba.org>
 * @version $Id: MDDParser.java,v 1.1 May 12, 2009 11:37:36 AM manu Exp $
 */
public class MDDParser extends MDDBaseParser {

    private MDDFactory factory = null;

    public MDDParser(TokenStream lexer, MDDFactory factory, String typeName, boolean included) {
        super(lexer);
        this.factory = factory;
        this.typeName = typeName;
        this.included = included;
    }

    protected Vector<String> includedFieldNames = new Vector<String>();

    protected HashMap<AST, AST> parsedFunctions = new HashMap<AST, AST>();

    @Override
    protected AST include(AST type) {
        AST included = null;
        try {
            included = factory.parseIncludedDataDefinition(type.getText()).getAST();
        } catch (DataDefinitionNotFoundError e) {
            factory.doThrow(typeName, "Could not find included data definition", type);
        }

        return included;

    }

    @Override
    protected AST includeSubField(AST type, AST parentField) {
        AST included = include(type);
        AST transformed = transformToSubfield(parentField, included);
        return transformed;
    }

    private AST transformToSubfield(AST parentField, AST included) {
        // process the included AST so that it fits the subField structure parentName -> field = type
        AST t = included;
        AST result = null;

        while (t != null) {

            // subfield1
            // |
            // |-- parentFieldName
            // | |
            // | titleField
            // |
            // subfield2
            // |
            // |-- parentFieldName
            // | |
            // | subFieldName
            // | |
            // | subFieldType
            // |
            // subfield3
            // ...
            MDDAST subfield = new MDDAST();
            subfield.setText("->!");
            subfield.setType(MDDTokenTypes.SUBFIELD);
            subfield.setLine(parentField.getLine());
            subfield.wasIncluded = true;

            MDDAST parentFieldName = new MDDAST();
            parentFieldName.setText(parentField.getText());
            parentFieldName.setType(MDDTokenTypes.PARENTFIELDNAME);
            parentFieldName.setLine(parentField.getLine());
            parentFieldName.wasIncluded = true;

            subfield.setFirstChild(parentFieldName);

            // build tree
            if (t.getType() == MDDTokenTypes.TITLEFIELDFIELD || t.getType() == MDDTokenTypes.TITLEFIELDFUNCTION) {
                subfield.getFirstChild().setNextSibling(t);
            } else {
                t.getFirstChild().setType(MDDTokenTypes.SUBFIELDNAME);
                includedFieldNames.add(t.getText());
                subfield.getFirstChild().setNextSibling(t.getFirstChild());
            }

            if (result == null) {
                result = subfield;
            } else {
                getLastSibling(result).setNextSibling(subfield);
            }

            t = t.getNextSibling();
        }
        return result;
    }

    private AST getLastSibling(AST t) {
        while (t.getNextSibling() != null) {
            t = t.getNextSibling();
        }
        return t;
    }

    private HashMap<String, AST> fieldsToDisable = new HashMap<String, AST>();

    @Override
    protected void disableField(AST field) {
        fieldsToDisable.put(field.getText(), field);
    }

    /**
     * performs post-processing tasks of parsing:<br>
     * - deactivates disabled fields from inclusions<br>
     */
    protected void postProcess() {
        AST t = getAST();
        AST prec = null;
        while (t != null && t.getNextSibling() != null) {
            prec = t;
            t = t.getNextSibling();
            if (t.getType() == MDDTokenTypes.FIELD && fieldsToDisable.containsKey(t.getText())) {
                prec.setNextSibling(t.getNextSibling());
                fieldsToDisable.remove(t.getText());
            }
        }

        for (String key : fieldsToDisable.keySet()) {
            factory.doThrow(this.typeName, "Fields cannot have an empty body unless they override an included field",
                fieldsToDisable.get(key));
        }
    }

    @Override
    protected AST parseExpression(AST expression) {

        Reader in = new StringReader(expression.getText());
        MDDLexer lexer = new MDDLexer(in);
        MDDExpressionParser parser = new MDDExpressionParser(lexer, factory, typeName, expression);
        parser.setASTNodeClass("org.makumba.providers.datadefinition.mdd.MDDAST");
        try {
            parser.expression();
        } catch (RecognitionException e) {
            e.column = expression.getColumn() + e.column;
            e.line = expression.getLine();
            factory.doThrow(e, expression, typeName);
        } catch (TokenStreamException e) {
            factory.doThrow(e, expression, typeName);
        }
        if (parser.error != null) {
            RecognitionException e = parser.error;
            e.column = expression.getColumn() + e.column;
            e.line = expression.getLine();
            factory.doThrow(e, expression, typeName);
        }

        AST tree = parser.getAST();
        if (tree != null) {
            shift(tree, expression);
        }

        // System.out.println("/////////////// Expression parser");
        // MakumbaDumpASTVisitor visitor = new MakumbaDumpASTVisitor(false);
        // visitor.visit(parser.getAST());

        return parser.getAST();
    }

    @Override
    protected AST parseFunctionBody(AST expression) {

        // here we parse the function to see if it's okay
        // when the expression is a subquery, i.e. starts with SELECT, we add parenthesis around it
        boolean subquery = expression.getText().toUpperCase().startsWith("SELECT ");

        int offset = "SELECT ".length();
        if (subquery) {
            offset += 1;
        }

        String query = "SELECT " + (subquery ? "(" : "") + expression.getText() + (subquery ? ")" : "") + " FROM "
                + typeName + " makumbaGeneratedAlias";
        HqlParser parser = HqlParser.getInstance(query);
        try {
            parser.statement();
        } catch (RecognitionException e) {
            e.column = expression.getColumn() + e.column;
            e.line = expression.getLine();
            factory.doThrow(e, expression, typeName);
        } catch (TokenStreamException e) {
            factory.doThrow(e, expression, typeName);
        }

        if (parser.getError() != null) {
            if (parser.getError() instanceof RecognitionException) {
                RecognitionException e = (RecognitionException) parser.getError();
                e.column = expression.getColumn() + e.column - offset;
                e.line = expression.getLine();
                factory.doThrow(e, expression, typeName);
            }
        }

        AST tree = parser.getAST();
        if (tree != null) {
            shiftHql(tree, expression, offset);
        }

        /* FIXME we can't do this here because then we want to access the MDD from within the inliner and it doesn't exist yet
        // now that we did the parsing, we also try to inline the AST
        // that way we will get the errors thrown by the inlining here and can display them nicely
        FunctionInliner inliner = new FunctionInliner(query);
        
        try {
            inliner.inlineFunctions(parser.getAST(), true);
        } catch(Throwable t) {
            factory.doThrow(typeName, t.getMessage(), parser.getAST());
        }
        */

        return parser.getAST();

    }

    private void shift(AST toShift, AST parent) {
        ((MDDAST) toShift).setLine(parent.getLine());
        ((MDDAST) toShift).setCol(parent.getColumn() + toShift.getColumn());

        if (toShift.getNextSibling() != null) {
            shift(toShift.getNextSibling(), parent);
        }
        if (toShift.getFirstChild() != null) {
            shift(toShift.getFirstChild(), parent);
        }
    }

    private void shiftHql(AST toShift, AST parent, int offset) {
        ((Node) toShift).setLine(parent.getLine());
        ((Node) toShift).setCol(parent.getColumn() + toShift.getColumn() + offset);

        if (toShift.getNextSibling() != null) {
            shiftHql(toShift.getNextSibling(), parent, offset);
        }
        if (toShift.getFirstChild() != null) {
            shiftHql(toShift.getFirstChild(), parent, offset);
        }
    }

    @Override
    protected void errorNestedSubfield(AST s) {
        factory.doThrow(typeName, "Nested subtypes are not allowed", s);
    }

    @Override
    protected void checkFieldName(AST fieldName) {

        String nm = fieldName.getText();

        for (int i = 0; i < nm.length(); i++) {
            if (i == 0 && !Character.isJavaIdentifierStart(nm.charAt(i)) || i > 0
                    && !Character.isJavaIdentifierPart(nm.charAt(i))) {
                factory.doThrow(this.typeName, "Invalid character \"" + nm.charAt(i) + "\" in field name \"" + nm,
                    fieldName);
            }
        }

        if (ReservedKeywords.isReservedKeyword(nm)) {
            factory.doThrow(this.typeName, "Error: field name cannot be one of the reserved keywords "
                    + ReservedKeywords.getKeywordsAsString(), fieldName);
        }
    }

    @Override
    protected void addParsedFunction(AST a, AST b) {
        parsedFunctions.put(a, b);
    }

}
